/**
 * This module is an extension to jQuery ui selectable, which scrolls when you try to select
 * outside the container, and the scrollables are not fitting it originally.
 *
 * Created by László Károlyi <http://linkedin.com/in/karolyi>
 * with the GPLv3 License: http://opensource.org/licenses/GPL-3.0
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see </licenses/>.
 */
/*global jQuery*/
(function ($) {
    'use strict';
    $.widget('ui.selectableScroll', $.ui.selectable, {
        options: {
            // If an element is passed in here, use it for scrolling instead of widget's element
            scrollElement: null,
            // When the selection is that pixels near to the top/bottom edges, start to scroll
            scrollSnapX: 5,
            // When the selection is that pixels near to the side edges, start to scroll
            scrollSnapY: 5,
            scrollAmount: 25, // In pixels
            scrollIntervalTime: 100 // In milliseconds
        },

        /**
         * This is a slightly modified version of the original _create function,
         * we just add the element's relative positions too
         *
         * @return null
         */
        _create: function () {
            this.element.addClass('ui-selectable');
            this.dragged = false;
            this.helperClasses = ['no-top', 'no-right', 'no-bottom', 'no-left'];
            this.scrollElement = this.options.scrollElement || this.element;
            this.refresh();
            this._mouseInit();
            this.helper = $('<div class="ui-selectable-helper"></div>');
        },

        /**
         * Cache selectee children based on filter
         * @return null
         */
        refresh: function () {
            var elementOffset = this.scrollElement.offset();
            var scrollLeft = this.scrollElement.prop('scrollLeft');
            var scrollTop = this.scrollElement.prop('scrollTop');
            this.selectees = $(this.options.filter, this.scrollElement[0]);
            this.selectees.addClass('ui-selectee');
            this.selectees.each(function () {
                var $element = $(this),
                    pos = $element.offset();
                $.data(this, 'selectable-item', {
                    element: this,
                    $element: $element,
                    left: pos.left,
                    top: pos.top,
                    right: pos.left + $element.outerWidth(),
                    bottom: pos.top + $element.outerHeight(),
                    // Relative positions according to the element's 0.0
                    relative: {
                        left: pos.left - elementOffset.left + scrollLeft,
                        top: pos.top - elementOffset.top + scrollTop,
                        right: pos.left - elementOffset.left + scrollLeft +
                        $element.outerWidth(),
                        bottom: pos.top - elementOffset.top + scrollTop +
                        $element.outerHeight()
                    },
                    startselected: false,
                    selected: $element.hasClass('ui-selected'),
                    selecting: $element.hasClass('ui-selecting'),
                    unselecting: $element.hasClass('ui-unselecting')
                });
            });
        },

        /**
         * By starting dragging, calculate the element's current scroll states,
         * relative to the elements 0.0 offset
         *
         * @param  object  The mousedown event
         * @return boolean The parent's _mouseStart return value
         */
        _mouseStart: function (event) {
            var pos = this.scrollElement.offset();
            if (
                (event.pageX > this.scrollElement.prop('clientWidth') + pos.left) ||
                (event.pageY > this.scrollElement.prop('clientHeight') + pos.top)
            ) {
                return false;
            }
            var retValue = $.ui.selectable.prototype._mouseStart.call(this,
                event);
            this.lastDragEvent = null;
            this.scrollInfo = {
                // The element's 0.0 offset related to the document element
                elementOffset: this.scrollElement.offset(),
                // The maximum scrollable width (visible width + scrollLeft)
                scrollWidth: this.scrollElement.prop('scrollWidth'),
                // The maximum scrollable height (visible height + scrollTop)
                scrollHeight: this.scrollElement.prop('scrollHeight'),
                // The visible width (minus scrollbars)
                elementWidth: this.scrollElement.prop('clientWidth'),
                // The visible height (minus scrollbars)
                elementHeight: this.scrollElement.prop('clientHeight')
            };
            // Relative to element's 0
            this.scrollInfo.dragStartXPos = event.pageX - this.scrollInfo.elementOffset
                    .left + this.scrollElement.prop('scrollLeft');
            // Relative to element's 0
            this.scrollInfo.dragStartYPos = event.pageY - this.scrollInfo.elementOffset
                    .top + this.scrollElement.prop('scrollTop');
            this.scrollIntervalId = null;
            return retValue;
        },

        /**
         * Calculate/redraw the helper lasso, keep the helper within
         * the scrolled element
         *
         * @param  object The options object containing the relative positions of the selected rectangle
         * @return null
         */
        _updateHelper: function (options) {
            var x1, y1, x2, y2; // Absolute positions for the lasso helper
            var lassoClassesArray = [];
            if (options.x1 - options.scrollLeft < 0) {
                lassoClassesArray.push('no-left');
                x1 = this.scrollInfo.elementOffset.left;
            } else {
                x1 = this.scrollInfo.elementOffset.left + options.x1 - options.scrollLeft;
            }
            if (options.y1 - options.scrollTop < 0) {
                lassoClassesArray.push('no-top');
                y1 = this.scrollInfo.elementOffset.top;
            } else {
                y1 = this.scrollInfo.elementOffset.top + options.y1 - options.scrollTop;
            }
            if (options.x2 - options.scrollLeft > this.scrollInfo.elementWidth) {
                lassoClassesArray.push('no-right');
                x2 = this.scrollInfo.elementOffset.left + this.scrollInfo.elementWidth;
            } else {
                x2 = this.scrollInfo.elementOffset.left + options.x2 - options.scrollLeft;
            }
            if (options.y2 - options.scrollTop > this.scrollInfo.elementHeight) {
                lassoClassesArray.push('no-bottom');
                y2 = this.scrollInfo.elementOffset.top + this.scrollInfo.elementHeight;
            } else {
                y2 = this.scrollInfo.elementOffset.top + options.y2 - options.scrollTop;
            }
            for (var counter = 0; counter < this.helperClasses.length; counter++) {
                var className = this.helperClasses[counter];
                if ($.inArray(className, lassoClassesArray) !== -1) {
                    this.helper.addClass(className);
                } else {
                    this.helper.removeClass(className);
                }
            }
            this.helper.css({
                left: x1,
                top: y1,
                width: x2 - x1,
                height: y2 - y1
            });
        },

        /**
         * If the mouse is near to the edges of the elements area, scroll
         *
         * @return object The new scrollLeft and scrollTop values, and a
         * boolean if the element should keep scrolling
         */
        _scrollIfNeeded: function (options) {
            var scrollLeft = this.scrollElement.prop('scrollLeft');
            var scrollTop = this.scrollElement.prop('scrollTop');

            var keepScrolling = false;
            // Scroll if close to edges or over them
            if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left <
                this.options.scrollSnapX && scrollLeft > 0) {
                scrollLeft = scrollLeft < this.options.scrollAmount ? 0 :
                    scrollLeft - this.options.scrollAmount;
                this.scrollElement.prop('scrollLeft', scrollLeft);
                keepScrolling = true;
            }
            if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top <
                this.options.scrollSnapY && scrollTop > 0) {
                scrollTop = scrollTop < this.options.scrollAmount ? 0 : scrollTop -
                    this.options.scrollAmount;
                this.scrollElement.prop('scrollTop', scrollTop);
                keepScrolling = true;
            }
            if (this.lastDragEvent.pageX - this.scrollInfo.elementOffset.left >
                this.scrollInfo.elementWidth - this.options.scrollSnapX && this.scrollInfo
                    .scrollWidth > scrollLeft + this.scrollInfo.elementWidth) {
                scrollLeft = scrollLeft + this.options.scrollAmount > this.scrollInfo
                    .scrollWidth - this.scrollInfo.elementWidth ? this.scrollInfo.scrollWidth -
                    this.scrollInfo.elementWidth : scrollLeft + this.options.scrollAmount;
                this.scrollElement.prop('scrollLeft', scrollLeft);
                keepScrolling = true;
            }
            if (this.lastDragEvent.pageY - this.scrollInfo.elementOffset.top >
                this.scrollInfo.elementHeight - this.options.scrollSnapY && this.scrollInfo
                    .scrollHeight > scrollLeft + this.scrollInfo.elementHeight) {
                scrollTop = scrollTop + this.options.scrollAmount > this.scrollInfo
                    .scrollHeight - this.scrollInfo.elementHeight ? this.scrollInfo
                        .scrollHeight - this.scrollInfo.elementHeight : scrollTop +
                    this.options.scrollAmount;
                this.scrollElement.prop('scrollTop', scrollTop);
                keepScrolling = true;
            }
            return {
                scrollLeft: scrollLeft,
                scrollTop: scrollTop,
                keepScrolling: keepScrolling
            };
        },

        /**
         * Calculate a relative position to the element's 0.0 offset,
         * from the original drag event's coordinates
         *
         * @param  object The absolute X and Y positions
         * @return object The relative X and Y positions
         */
        _calcRelativePosition: function (options) {
            var relXPos = options.x - this.scrollInfo.elementOffset.left +
                options.scrollLeft;
            var relYPos = options.y - this.scrollInfo.elementOffset.top +
                options.scrollTop;
            return {
                x: relXPos,
                y: relYPos
            };
        },

        /**
         * Calculate relative area positions to offset element
         *
         * @param  object The relative X and Y positions to the 0.0 of the element
         * @return object An object containing the relative area coordinates
         */
        _calcRelativeArea: function (options) {
            var relX1 = options.xPos < this.scrollInfo.dragStartXPos ? options.xPos :
                this.scrollInfo.dragStartXPos;
            var relY1 = options.yPos < this.scrollInfo.dragStartYPos ? options.yPos :
                this.scrollInfo.dragStartYPos;
            var relX2 = options.xPos > this.scrollInfo.dragStartXPos ? options.xPos :
                this.scrollInfo.dragStartXPos;
            var relY2 = options.yPos > this.scrollInfo.dragStartYPos ? options.yPos :
                this.scrollInfo.dragStartYPos;
            return {
                x1: relX1,
                y1: relY1,
                x2: relX2,
                y2: relY2
            };
        },

        /**
         * Update the selected elements
         *
         * @param  object The return value of _calcRelativeArea()
         * @return null
         */
        _updateSelectees: function (options) {
            var that = this;
            this.selectees.each(function () {
                var selectee = $.data(this, 'selectable-item'),
                    hit = false;

                // Prevent helper from being selected if appendTo: selectable
                if (!selectee || selectee.element === that.element[0]) {
                    return;
                }

                if (that.options.tolerance === 'touch') {
                    hit = (!(selectee.relative.left > options.x2 || selectee.relative
                        .right < options.x1 || selectee.relative.top > options.y2 ||
                    selectee.relative.bottom < options.y1));
                } else if (that.options.tolerance === 'fit') {
                    hit = (selectee.relative.left > options.x1 && selectee.relative
                        .right < options.x2 && selectee.relative.top > options.y1 &&
                    selectee.relative.bottom < options.y2);
                }

                if (hit) {
                    // SELECT
                    if (selectee.selected) {
                        selectee.$element.removeClass('ui-selected');
                        selectee.selected = false;
                    }
                    if (selectee.unselecting) {
                        selectee.$element.removeClass('ui-unselecting');
                        selectee.unselecting = false;
                    }
                    if (!selectee.selecting) {
                        selectee.$element.addClass('ui-selecting');
                        selectee.selecting = true;
                        // selectable SELECTING callback
                        that._trigger('selecting', that.lastDragEvent, {
                            selecting: selectee.element
                        });
                    }
                } else {
                    // UNSELECT
                    if (selectee.selecting) {
                        if ((that.lastDragEvent.metaKey || that.lastDragEvent.ctrlKey) &&
                            selectee.startselected) {
                            selectee.$element.removeClass('ui-selecting');
                            selectee.selecting = false;
                            selectee.$element.addClass('ui-selected');
                            selectee.selected = true;
                        } else {
                            selectee.$element.removeClass('ui-selecting');
                            selectee.selecting = false;
                            if (selectee.startselected) {
                                selectee.$element.addClass('ui-unselecting');
                                selectee.unselecting = true;
                            }
                            // Selectable UNSELECTING callback
                            that._trigger('unselecting', that.lastDragEvent, {
                                unselecting: selectee.element
                            });
                        }
                    }
                    if (selectee.selected) {
                        if (!that.lastDragEvent.metaKey && !that.lastDragEvent.ctrlKey && !
                                selectee.startselected) {
                            selectee.$element.removeClass('ui-selected');
                            selectee.selected = false;

                            selectee.$element.addClass('ui-unselecting');
                            selectee.unselecting = true;
                            // Selectable UNSELECTING callback
                            that._trigger('unselecting', that.lastDragEvent, {
                                unselecting: selectee.element
                            });
                        }
                    }
                }
            });
        },

        /**
         * The original _mouseDrag function overvritten by our one
         *
         * @param  object  The original mousemove event
         * @return boolean Returning false, as the parent returns too
         */
        _mouseDrag: function (event) {
            this.dragged = true;
            if (this.options.disabled) {
                return;
            }
            this.lastDragEvent = event;

            var scrollObj = this._scrollIfNeeded();

            this._updateIntervals({
                keepScrolling: scrollObj.keepScrolling
            });
            this._updateUi({
                doUpdateHelper: true
            });

            return false;
        },

        /**
         * Do the actual calculating/updating of the positions/elements
         *
         * @param  object Options containing if the function should update the helper lasso
         * @return null
         */
        _updateUi: function (options) {
            var scrollObj = this._scrollIfNeeded({
                pageX: this.lastDragEvent.pageX,
                pageY: this.lastDragEvent.pageY
            });
            var relativePos = this._calcRelativePosition({
                x: this.lastDragEvent.pageX,
                y: this.lastDragEvent.pageY,
                scrollLeft: scrollObj.scrollLeft,
                scrollTop: scrollObj.scrollTop
            });
            var relativeArea = this._calcRelativeArea({
                xPos: relativePos.x,
                yPos: relativePos.y
            });
            if (options.doUpdateHelper) {
                this._updateHelper({
                    scrollLeft: scrollObj.scrollLeft,
                    scrollTop: scrollObj.scrollTop,
                    x1: relativeArea.x1,
                    y1: relativeArea.y1,
                    x2: relativeArea.x2,
                    y2: relativeArea.y2
                });
            }
            this._updateSelectees({
                x1: relativeArea.x1,
                y1: relativeArea.y1,
                x2: relativeArea.x2,
                y2: relativeArea.y2
            });
        },

        /**
         * Start the automatic scrolling if needed
         *
         * @param  object Options containing if the function should start the interval
         * @return null
         */
        _updateIntervals: function (options) {
            var that = this;
            if (options.keepScrolling && !this.scrollIntervalId) {
                this.scrollIntervalId = setInterval(function () {
                    that._updateUi({
                        doUpdateHelper: false
                    });
                }, this.options.scrollIntervalTime);
            }
            if (!options.keepScrolling && this.scrollIntervalId)
                this._clearIntervals();
        },

        /**
         * Clear the autoscroll interval
         *
         * @return null
         */
        _clearIntervals: function () {
            // Stop scrolling
            if (this.scrollIntervalId)
                clearInterval(this.scrollIntervalId);
            this.scrollIntervalId = null;
        },

        /**
         * The original _mouseStop event extended with the interval clearer
         *
         * @param  object  The original mousestop event
         * @return boolean The parent's return value
         */
        _mouseStop: function (event) {
            this._clearIntervals();
            var retValue = $.ui.selectable.prototype._mouseStop.call(this,
                event);
            return retValue;
        }
    });
})(jQuery);
